iT邦幫忙

2025 iThome 鐵人賽

DAY 29
0
Software Development

30 天的 .Net gRPC 迷途系列 第 29

Day29 實作 TODO List (Client)

  • 分享至 

  • xImage
  •  

完成了 Server 端,接著請 AI 幫忙畫了下 UI 介面並提供 .proto 檔請他完成

https://ithelp.ithome.com.tw/upload/images/20250922/201778843o4gXRFjlZ.png

可以說完成度是非常高,幾乎是貼上即可使用(還是有零星 Bug)

只能說在 AI 協作之下,這種規格導向開發是快速許多

整個 Form1.cs 如下

public partial class Form1 : Form
{
    private TodoService.TodoServiceClient _client;
    private TodoItem _selectedTodo;

    public Form1()
    {
        InitializeComponent();
        InitializeGrpcClient();
        LoadTodos();
    }

    private void InitializeGrpcClient()
    {
        // 假設 gRPC Server 跑在 localhost:50051
        var channel = GrpcChannel.ForAddress("https://localhost:7204");
        _client = new TodoService.TodoServiceClient(channel);
    }

    private async void LoadTodos()
    {
        try
        {
            var req = new ListTodosReq();
            if (chkFilterCompleted.Checked)
            {
                req.FilterCompleted = false;
            }

            var res = await _client.ListTodosAsync(req);
            dgvTodos.DataSource = res.Todos.Select(t => new
            {
                t.Id,
                t.Title,
                t.Description,
                IsCompleted = t.IsCompleted ? "✅" : "❌",
                CreatedTime = t.CreatedTime?.ToDateTime() ?? DateTime.MinValue
            }).ToList();

            // 設定欄位名稱與格式
            dgvTodos.Columns["Id"].HeaderText = "ID";
            dgvTodos.Columns["Title"].HeaderText = "標題";
            dgvTodos.Columns["Description"].HeaderText = "描述";
            dgvTodos.Columns["IsCompleted"].HeaderText = "完成";
            dgvTodos.Columns["CreatedTime"].HeaderText = "建立時間";
            dgvTodos.Columns["CreatedTime"].DefaultCellStyle.Format = "yyyy-MM-dd HH:mm:ss";

            dgvTodos.ClearSelection();
            _selectedTodo = null;
            ResetForm();
        }
        catch (Exception ex)
        {
            MessageBox.Show($"載入失敗:{ex.Message}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    private void ResetForm()
    {
        txtTitle.Clear();
        txtDescription.Clear();
        chkIsCompleted.Checked = false;
        btnUpdate.Enabled = false;
        btnDelete.Enabled = false;
    }

    private void dgvTodos_SelectionChanged(object sender, EventArgs e)
    {
        if (dgvTodos.SelectedRows.Count > 0)
        {
            var row = dgvTodos.SelectedRows[0];
            var id = (int)row.Cells["Id"].Value;

            // 重新取得最新資料(避免 UI 與資料不同步)
            _ = Task.Run(async () =>
            {
                try
                {
                    var res = await _client.GetTodoAsync(new GetTodoReq { Id = id });
                    if (res.Todo != null)
                    {
                        Invoke((Action)(() =>
                        {
                            _selectedTodo = res.Todo;
                            txtTitle.Text = _selectedTodo.Title;
                            txtDescription.Text = _selectedTodo.Description;
                            chkIsCompleted.Checked = _selectedTodo.IsCompleted;
                            btnUpdate.Enabled = true;
                            btnDelete.Enabled = true;
                        }));
                    }
                }
                catch (Exception ex)
                {
                    Invoke((Action)(() =>
                    {
                        MessageBox.Show($"取得資料失敗:{ex.Message}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }));
                }
            });
        }
        else
        {
            ResetForm();
        }
    }

    private async void btnCreate_Click(object sender, EventArgs e)
    {
        if (string.IsNullOrWhiteSpace(txtTitle.Text))
        {
            MessageBox.Show("標題不可為空", "輸入錯誤", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            return;
        }

        try
        {
            var req = new CreateTodoReq
            {
                Title = txtTitle.Text.Trim(),
                Description = txtDescription.Text.Trim()
            };

            var res = await _client.CreateTodoAsync(req);
            MessageBox.Show($"新增成功!ID: {res.Todo.Id}", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
            LoadTodos();
        }
        catch (Exception ex)
        {
            MessageBox.Show($"新增失敗:{ex.Message}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    private async void btnUpdate_Click(object sender, EventArgs e)
    {
        if (_selectedTodo == null)
        {
            MessageBox.Show("請先選擇項目", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            return;
        }

        try
        {
            var req = new UpdateTodoReq
            {
                Id = _selectedTodo.Id,
                Title = txtTitle.Text.Trim(),
                Description = txtDescription.Text.Trim(),
                IsCompleted = chkIsCompleted.Checked
            };

            var res = await _client.UpdateTodoAsync(req);
            MessageBox.Show($"更新成功!", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
            LoadTodos();
        }
        catch (Exception ex)
        {
            MessageBox.Show($"更新失敗:{ex.Message}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    private async void btnDelete_Click(object sender, EventArgs e)
    {
        if (_selectedTodo == null)
        {
            MessageBox.Show("請先選擇項目", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            return;
        }

        var confirm = MessageBox.Show($"確定刪除「{_selectedTodo.Title}」?", "確認刪除", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
        if (confirm != DialogResult.Yes) return;

        try
        {
            var req = new DeleteTodoReq { Id = _selectedTodo.Id };
            var res = await _client.DeleteTodoAsync(req);

            if (res.Success)
            {
                MessageBox.Show("刪除成功!", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
                LoadTodos();
            }
            else
            {
                throw new Exception("伺服器回傳刪除失敗");
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show($"刪除失敗:{ex.Message}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    private void chkFilterCompleted_CheckedChanged(object sender, EventArgs e)
    {
        LoadTodos();
    }
}

上一篇
Day28 實作 TODO List (Server)
系列文
30 天的 .Net gRPC 迷途29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言